/**
 * @author Janusz Dziemidowicz rraptorr@nails.eu.org
 * @fileoverview All stuff related to WebSocket
 * <pre>
 * The WebSocket protocol is a bit of a mess. Various, incompatible,
 * protocol drafts were implemented in browsers. Fortunately, recently
 * a finished protocol was released in RFC6455. Further description
 * assumes RFC6455 WebSocket protocol version.
 *
 * WebSocket browser support. Current (November 2012) browser status:
 * - Chrome 16+ - works properly and supports RFC6455
 * - Firefox 16+ - works properly and support RFC6455 (ealier versions
 *   have problems with proxies)
 * - Opera 12.10 - supports RFC6455, but does not work at all if a
 *   proxy is configured (earlier versions do not support RFC6455)
 * - Internet Explorer 10+ - works properly and supports RFC6455
 *
 * Due to the above status, this code is currently recommended on
 * Chrome 16+, Firefox 16+ and Internet Explorer 10+. Using it on
 * other browsers is discouraged.
 *
 * Please also note that some users are only able to connect to ports
 * 80 and 443. Port 80 is sometimes intercepted by transparent HTTP
 * proxies, which mostly does not support WebSocket, so port 443 is
 * the best choice currently (it does not have to be
 * encrypted). WebSocket also usually does not work well with reverse
 * proxies, be sure to make extensive tests if you use one.
 *
 * There is no standard for XMPP over WebSocket. However, there is a
 * draft (http://tools.ietf.org/html/draft-ietf-xmpp-websocket-00) and
 * this implementation follows it.
 *
 * Tested servers:
 *
 * - node-xmpp-bosh (https://github.com/dhruvbird/node-xmpp-bosh) -
 *   supports RFC6455 and works with no problems since 0.6.1, it also
 *   transparently uses STARTTLS if necessary
 * - wxg (https://github.com/Gordin/wxg) - supports RFC6455 and works
 *   with no problems, but cannot connect to servers requiring
 *   STARTTLS (original wxg at https://github.com/hocken/wxg has some
 *   issues, that were fixed by Gordin).
 * - ejabberd-websockets
 *   (https://github.com/superfeedr/ejabberd-websockets) - does not
 *   support RFC6455 hence it does not work, adapting it to support
 *   RFC6455 should be quite easy for anyone knowing Erlang (some work
 *   in progress can be found on github)
 * - Openfire (http://www.igniterealtime.org/projects/openfire/) -
 *   unofficial plugin is available, but it lacks support
 *   for RFC6455 hence it does not work
 * - Apache Vysper (http://mina.apache.org/vysper/) - does
 *   not support RFC6455 hence does not work
 * - Tigase (http://www.tigase.org/) - works fine since 5.2.0.
 * - MongooseIM (https://github.com/esl/ejabberd) - a fork of ejabberd
 *   with support for XMPP over Websockets.
 * </pre>
 */

import  {JSJaCConnection, ConnectionParam, LoginParam } from "./JSJaCConnection";
import {JSJaC} from "./JSJaC";
import {JSJaCError} from "./JSJaCError";
import {XmlUtil} from "./XmlUtil";
import { NS_STREAM, NS_FRAMING, NS_STREAMS } from "./JSJaCConstants";
import {JSJaCCookie} from "./JSJaCCookie";
import {JSJaCPacket} from "./JSJaCPacket";
import {IXMLDocument} from "./IXMLDocument";

/*exported JSJaCWebSocketConnection */

/**
 * Instantiates a WebSocket session.
 * @class Implementation of {@link http://tools.ietf.org/html/draft-ietf-xmpp-websocket-00 | An XMPP Sub-protocol for WebSocket}.
 * @extends JSJaCConnection
 * @constructor
 * @param {Object} oArg connection properties.
 * @param {string} oArg.httpbase WebSocket connection endpoint (i.e. ws://localhost:5280)
 * @param {JSJaCDebugger} [oArg.oDbg] A reference to a debugger implementing the JSJaCDebugger interface.
 */
export class JSJaCWebSocketConnection extends JSJaCConnection {
  private _ws:WebSocket;
  constructor(oArg:ConnectionParam) {
    super(oArg);
    this.registerHandler('onerror', JSJaC.bind(this._cleanupWebSocket, this));
  }
  protected _getFreeSlot(): number {
    return -1;
    //throw new Error("Method not implemented.");
  }
  protected _setupRequest(async: boolean) {
    console.debug('_setupRequest do nothing');
  }
  protected _getInitialRequestString(): string {

    let streamto:string;
    let reqstr:string;
    streamto = this.domain;
    if (this.authhost) {
      streamto = this.authhost;
    }

    reqstr = '<open xmlns="' + NS_FRAMING + '" to="' + streamto + '" version="1.0"/>';
    return reqstr;

  }
  protected _getRequestString(raw: string, last: boolean): string {
    return '</stream:stream>'
  }
  protected _reInitStream(cb: Function): void {
    let reqstr:string, streamto:string = this.domain;
    if (this.authhost) {
      streamto = this.authhost;
    }

    reqstr = '<open xmlns="' + NS_FRAMING + '" to="' + streamto + '" version="1.0"/>';
    this._sendRaw(reqstr, cb);
  }
  protected _parseResponse(r: any): IXMLDocument {
    throw new Error("Method not implemented.");
  }
private _cleanupWebSocket():void {
  if (this._ws !== null) {
    this._ws.onclose = null;
    this._ws.onerror = null;
    this._ws.onopen = null;
    this._ws.onmessage = null;

    this._ws.close();
    this._ws = null;
  }
};
/**
 * Connect to a jabber/XMPP server.
 * @param {Object} oArg The configuration to be used for connecting.
 * @param {string} oArg.domain The domain name of the XMPP service.
 * @param {string} oArg.username The username (nodename) to be logged in with.
 * @param {string} oArg.resource The resource to identify the login with.
 * @param {string} oArg.password The user's password.
 * @param {string} [oArg.authzid] Authorization identity. Used to act as another user, in most cases not needed and rarely supported by servers. If present should be a bare JID (user@example.net).
 * @param {boolean} [oArg.allow_plain] Whether to allow plain text logins.
 * @param {boolean} [oArg.allow_scram] Whether to allow SCRAM-SHA-1 authentication. Please note that it is quite slow, do some testing on all required browsers before enabling.
 * @param {boolean} [oArg.register] Whether to register a new account.
 * @param {string} [oArg.authhost] The host that handles the actualy authorization. There are cases where this is different from the settings above, e.g. if there's a service that provides anonymous logins at 'anon.example.org'.
 * @param {string} [oArg.authtype] Must be one of 'sasl' (default), 'nonsasl', 'saslanon', or 'anonymous'.
 * @param {string} [oArg.xmllang] The requested language for this login. Typically XMPP server try to respond with error messages and the like in this language if available.
 */
connect(oArg:LoginParam):void {
  super.connect(oArg);
  if(this._ws ==null){
     if (typeof WebSocket === 'undefined') {
       this._handleEvent('onerror', new JSJaCError('503', 'cancel', 'service-unavailable'));
       return;
     }
     this.oDbg.debug('create websocket client');
     this._ws = new WebSocket(this._httpbase, 'xmpp');
   } else {
     this.oDbg.debug('used constructor ws connection')
   }
  this._ws.onclose = () =>{this._onclose()};
  this._ws.onerror = ()=>{ this._onerror();}
  this._ws.onopen = ()=>{this._onopen()};
};
/**
 * 在JsJaCSDKWebSocketConnection子类中调用
 * @param ws 送ws
 */
protected  setWs(ws:WebSocket):void {
  this._ws=ws;
}

/**
 * Disconnect from XMPP service
 *
 * When called upon leaving a page needs to use 'onbeforeunload' event
 * as Websocket would be closed already otherwise prior to this call.
 */
disconnect() {
  this._setStatus('disconnecting');

  if (!this.connected()) {
    return;
  }
  this._connected = false;

  this.oDbg.debug('Disconnecting');
  this._sendRaw('<close xmlns="' + NS_FRAMING + '"/>', JSJaC.bind(this._cleanupWebSocket, this));

  this.oDbg.info('Disconnected');
  this._handleEvent('ondisconnect');
};

/**
 * @private
 */
private _onclose() {
  this.oDbg.info('websocket closed');
  if (this._status !== 'disconnecting') {
    this._connected = false;
    this._handleEvent('onerror', new JSJaCError('503', 'cancel', 'service-unavailable'));
  }
};

/**
 * @private
 */
private _onerror() {
  this.oDbg.error('websocket error');
  this._connected = false;
  this._handleEvent('onerror', new JSJaCError('503', 'cancel', 'service-unavailable'));
};
/**
 * @private
 */
protected _onopen() {
  let reqstr:string = this._getInitialRequestString();
  this.oDbg.debug(reqstr);
  this._ws.onmessage =(ev:MessageEvent) =>{ this._handleOpenStream(ev)};
  this._ws.send(reqstr);
};

/**
 * @private
 */
private _handleOpenStream(event:MessageEvent):void {
  this.oDbg.debug(event.data);
  let open = event.data;
  let stream:IXMLDocument= this._parseXml(open);
  if(!stream) {
    this._handleEvent('onerror', new JSJaCError('503', 'cancel', 'service-unavailable'));
    return;
  }

  // extract stream id used for non-SASL authentication
  this.streamid = stream.getAttribute('id');

  this.oDbg.info('got streamid: ' + this.streamid);
  this._ws.onmessage = (ev:MessageEvent) =>{ this._handleInitialResponse(ev)};
};
/**
 * @private
 */
private _handleInitialResponse (event:MessageEvent) {
  let doc:IXMLDocument = this._parseXml(event.data);
  if (!this._parseStreamFeatures(doc)) {
    this._handleEvent('onerror', new JSJaCError('503', 'cancel', 'service-unavailable'));
    return;
  }

  this._connected = true;

  if (this.register) {
    this._doInBandReg();
  } else {
    this._doAuth();
  }
};

/**
 * Parse single XML stanza. As proposed in XMPP Sub-protocol for
 * WebSocket draft, it assumes that every stanza is sent in a separate
 * WebSocket frame, which greatly simplifies parsing.
 */
private _parseXml(s:string):IXMLDocument {
  let doc:IXMLDocument;

  this.oDbg.debug('Parsing: ' + s);
  try {
    doc = XmlUtil.create('stream', NS_STREAM);
    if(s.lastIndexOf('<close ', 0) === 0) {
      // Consider session as closed
      this.oDbg.warn("session terminated");

      clearTimeout(this._timeout); // remove timer
      clearInterval(this._interval);
      clearInterval(this._inQto);

      try {
          JSJaCCookie.read(this._cookie_prefix+'JSJaC_State').erase();
      } catch (e) {}

      this._connected = false;
      this._handleEvent('onerror',new JSJaCError('503','cancel','session-terminate'));

      this.oDbg.info("Disconnected.");
      this._handleEvent('ondisconnect');
      doc=XmlUtil.loadXML('<xml></xml>');
      return doc;
    } else {
      doc=XmlUtil.loadXML(s)
      return doc;
    }
  } catch (e) {
    this.oDbg.error('Error: ' + e);
    this._connected = false;
    this._handleEvent('onerror', new JSJaCError('500', 'wait', 'internal-service-error'));
  }

  return null;
};
  /**
   * @private
   */
  private _onmessage(event:MessageEvent):void {
    let stanza:any;
    let packet:JSJaCPacket;
    let node:IXMLDocument;
  
    stanza = event.data;
    this._setStatus('processing');
    if (!stanza || stanza === '') {
      return;
    }
  
    // WebSocket works only on modern browsers, so it is safe to assume
    // that namespaceURI and getElementsByTagNameNS are available.
    node = this._parseXml(stanza);
    if (node  && node.getLocalName() === 'error' && node.getNamespaceURI() === NS_STREAM ) {
      if (node.getElementsByTagNameNS(NS_STREAMS, 'conflict').length > 0) {
        this._setStatus('session-terminate-conflict');
      }
      this._connected = false;
      this._handleEvent('onerror',  new JSJaCError('503', 'cancel', 'remote-stream-error'));
      return;
    }
  
    packet = this.wrapNode(node);
    if (!packet) {
      return;
    }
  
    this.oDbg.info('async recv: ' + event.data);
    this._handleEvent('packet_in', packet);
  
    if (packet.pType && !this._handlePID(packet)) {
      this._handleEvent(packet.pType() + '_in', packet);
      this._handleEvent(packet.pType(), packet);
    }
  };
  /**
   * 调用发送消息到客户端
   * @param packet 
   * @param cb  回调函数
   * @param arg 回调函数参数
   * @returns 
   */
  send(packet:JSJaCPacket, cb?:Function, arg?:any):boolean {
    if (!this.connected()) {
      return false;
    }
  
    if (!packet || !packet.pType) {
      this.oDbg.warn('no packet: ' + packet);
      return false;
    }
    this._ws.onmessage = (ev:MessageEvent) =>{this._onmessage(ev);}
  
    // remember id for response if callback present
    if (cb) {
      if (!packet.getID()) {
        packet.setID('JSJaCID_' + this._ID++); // generate an ID
      }
  
      // register callback with id
      this._registerPID(packet, cb, arg);
    }
  
    try {
      this._handleEvent(packet.pType() + '_out', packet);
      this._handleEvent('packet_out', packet);
      this._ws.send(packet.xml());
    } catch (e) {
      this.oDbg.error(e.toString());
      return false;
    }
    return true;
  };
  
  /**
   * Resuming connections is not supported by WebSocket.
   */
  resume():boolean {
    return false; // not supported for websockets
  };

/**
 * Suspending connections is not supported by WebSocket.
 */
suspend():boolean {
  return false; // not supported for websockets
};

/**
 * @private
 */
// private _doSASLAuthScramSha1S1(event:MessageEvent):Function {
//   var el = this._parseXml(event.data);
//   return JSJaC.bind(super._doSASLAuthScramSha1S1, this)(el);
// };

/**
 * @private
 */
// private _doSASLAuthScramSha1S2(event:MessageEvent):Function {
//   var el = this._parseXml(event.data);
//   return JSJaC.bind(super._doSASLAuthScramSha1S2, this)(el);
// };

/**
 * @private
 */
//  private _doSASLAuthDigestMd5S1(event:MessageEvent):Function {
//   var el = this._parseXml(event.data);
//   return JSJaC.bind(super._doSASLAuthDigestMd5S1, this)(el);
// };

/**
 * @private
 */
protected _doSASLAuthDigestMd5S2(event:MessageEvent):Function {
  var el = this._parseXml(event.data);
  return JSJaC.bind(super._doSASLAuthDigestMd5S2, this)(el);
};

/**
 * @private
 */
protected _doSASLAuthDone(event:MessageEvent):Function {
  var el = this._parseXml(event.data);
  return JSJaC.bind(super._doSASLAuthDone, this)(el);
};
/**
 * @private
 */
protected _sendRaw(xml:string, cb:Function, arg?:any):boolean {
  if (!this._ws) {
    // Socket might have been closed already because of an 'onerror'
    // event. In this case we'd try to send a closing stream element
    // 'ondisconnect' which won't work.
    return false;
  }
  if (cb) {
    this._ws.onmessage =(event:MessageEvent) =>{
      cb(event);
    }
  }
  this._ws.send(xml);
  return true;
};

}